12장 딱 하나뿐인 딸린 자원 (싱글톤 서브리소스) — 입문판
출처: API Design Patterns (Manning) · JJ Geewax | 참고: https://google.aip.dev/156
이 장에서 딱 4가지만 기억하면 됩니다. 1. 하나의 자원 안에 성격이 너무 다른 데이터가 섞여 있으면 떼어내자 (왜 떼어내는가) 2. 떼어낸 그 조각이 '속성도 아니고 완전한 자원도 아닌 중간 형태'라는 것 (그게 뭔가) 3. 그 중간 형태는 조회·수정만 따로 하고, 생성·삭제는 부모를 따라간다는 것 (어떻게 동작하나) 4. 이 조각은 반드시 부모 밑에 있어야 한다는 것 (어디에 두나)
학습 목표
이 장을 마치면 다음을 할 수 있습니다.
- 자원의 일부 데이터를 따로 떼어내야 하는 세 가지 상황을 설명한다.
- 싱글톤 서브리소스가 단순 속성, 완전한 자원과 어떻게 다른지 구분한다.
- 싱글톤 서브리소스에서 어떤 표준 메서드가 동작하고 어떤 것이 동작하지 않는지 설명한다.
- 삭제 대신 reset을 쓰는 이유를 설명한다.
- 싱글톤 서브리소스를 자원 계층 어디에 두어야 하는지 설명한다.
12.1 한 자원에 다 넣었더니 터지는 순간
먼저, 이런 적 있죠?
승차 공유 앱을 만든다고 해봅시다. 운전기사 한 명을 표현하려고 자원 하나를 이렇게 짰어요.
Driver {
id: "drivers/1"
name: "홍길동"
licensePlate: "12가3456"
lat: 37.5665 ← 차가 움직이는 동안 1초마다 바뀜
long: 126.9780 ← 차가 움직이는 동안 1초마다 바뀜
}
언뜻 깔끔해 보이죠. 그런데 운영하다 보면 자꾸 불편한 일이 생깁니다.
- 기사 이름만 보고 싶은데, 호출할 때마다 위치 좌표까지 딸려옵니다.
- 위치 한 칸만 바꾸고 싶은데, Driver 전체를 수정하는 요청을 보내야 합니다.
- 이름은 거의 안 바뀌는데 위치는 1초마다 바뀝니다. 둘이 한 자원에 묶여 있으니 위치 갱신이 들어올 때마다 같은 자원을 건드리게 됩니다.
이게 바로 "한 자원에 성격 다른 데이터를 다 욱여넣어서 터지는" 장면입니다.
그게 바로 '분리'가 필요한 순간이다
해결책은 단순합니다. 성격이 너무 다른 데이터는 자원에서 떼어내 따로 두자는 것.
새 용어: 서브리소스(sub-resource) — 어떤 자원에 딸려 있는 자식 자원입니다. 일상 비유로 보면, 사람(부모)에게 딸린 '지갑'(자식)처럼 본체에 붙어 다니지만 따로 열어볼 수 있는 것입니다.
좀 전 예시를 이렇게 고칠 수 있습니다.
Driver {
id: "drivers/1"
name: "홍길동"
licensePlate: "12가3456"
} ← 위치는 여기서 빠졌습니다
DriverLocation {
id: "drivers/1/location" ← 부모(drivers/1)에 딸린 자식
lat: 37.5665
long: 126.9780
}
이제 이름을 볼 때는 위치가 안 따라오고, 위치를 바꿀 때는 Driver 전체를 건드릴 필요가 없습니다.
떼어내는 이유 딱 3가지
데이터를 떼어낼지 말지는 다음 세 가지 신호로 판단합니다.
이유 1 — 크기 (너무 무겁다)
새 용어: 메타데이터(metadata) — '데이터에 대한 설명 데이터'입니다. 일상 비유로 보면, 택배 상자 안의 물건이 본 데이터라면 상자에 붙은 송장(보낸 사람, 무게, 받는 곳)이 메타데이터입니다.
사진 같은 큰 파일을 자원에 통째로 넣어두면, 이름만 확인하려 해도 큰 파일까지 같이 내려받게 됩니다.
# 떼어내기 전 — 메타데이터만 보고 싶은데 수 GB가 딸려옴
GetObject() → 송장 정보 + 실제 파일(수 GB) ← 낭비
# 떼어낸 후 — 필요한 것만
GetObjectMetadata() → 송장 정보만 (수 KB)
GetObjectData() → 실제 파일만 (정말 필요할 때만)
이유 2 — 보안 (보는 사람이 다르다)
직원 정보 안에 급여가 같이 들어 있으면, 직원 정보를 가져오는 모든 사람이 급여까지 보게 됩니다.
# 떼어내기 전 — 위험
GetEmployee() → 이름, 부서, 전화번호, 급여(!) ← 누구나 봄
# 떼어낸 후 — 급여만 따로
GetEmployee() → 이름, 부서, 전화번호 (일반 직원도 조회)
GetEmployeeSalary() → 급여 (인사팀만 조회)
이유 3 — 휘발성 (바뀌는 속도가 다르다)
새 용어: 휘발성(volatility) — 데이터가 얼마나 자주 바뀌는지를 뜻합니다. 일상 비유로 보면, 사람의 '이름'은 평생 거의 안 바뀌지만 '지금 위치'는 걸을 때마다 바뀌는 것과 같습니다. 자주 바뀌는 쪽이 휘발성이 높습니다.
새 용어: 쓰기 경합(write contention) — 여러 요청이 같은 자원을 동시에 고치려고 몰릴 때 생기는 충돌입니다. 일상 비유로 보면, 한 개의 화이트보드에 여러 명이 동시에 쓰려고 펜을 들이대 서로 밀치는 상황입니다.
이름은 거의 안 바뀌는데 위치는 매초 바뀝니다. 둘을 한 자원에 묶어두면 위치 갱신이 쏟아질 때마다 같은 자원으로 쓰기가 몰려 경합이 생깁니다.
# 떼어낸 후 — 서로 방해 안 함
UpdateDriver() → 이름, 차량번호 (가끔)
UpdateDriverLocation() → 위치 (매초)
비유로 한눈에 — 표로 묶기
| 일상 비유 | API/코드 상황 | 위험·주의 |
|---|---|---|
| 송장만 보려는데 택배 물건까지 받음 | GetObject가 수 GB 파일까지 반환 | 작은 정보 보려고 큰 전송 비용 발생 → 크기로 분리 |
| 직원 명단에 급여가 같이 적힘 | GetEmployee가 급여까지 반환 | 권한 없는 사람이 민감 정보 열람 → 보안으로 분리 |
| 한 화이트보드에 여러 명이 동시에 씀 | 위치 갱신이 Driver 자원에 몰림 | 쓰기 경합으로 충돌·지연 → 휘발성으로 분리 |
여기까지 체크포인트
- 크기가 너무 큰 조각은 떼어낸다.
- 보는 사람(권한)이 다른 조각은 떼어낸다.
- 바뀌는 속도가 크게 다른 조각은 떼어낸다.
- 핵심은 "같은 자원 안인데 성격이 다른 데이터"를 가려내는 것이다.
12.2 떼어낸 그 조각의 정체 — 속성과 자원의 중간
먼저, 이런 적 있죠?
떼어내긴 했는데, "이걸 뭐라고 불러야 하지?" 하고 헷갈린 적 있을 겁니다. 완전히 독립된 자원이라기엔 부모 없이는 존재할 수 없고, 그냥 속성이라기엔 따로 조회·수정이 됩니다.
그게 바로 '싱글톤 서브리소스'다
새 용어: 싱글톤 서브리소스(singleton sub-resource) — 부모 자원에 딱 하나만 딸려 있는 특별한 자식 자원입니다. 속성처럼 부모와 함께 생기고 사라지지만, 자원처럼 따로 조회·수정할 수 있는 중간 형태입니다. 일상 비유로 보면, 스마트폰의 '배터리'처럼 폰에 딱 하나 들어 있고 폰과 함께 생기지만 충전량은 따로 들여다볼 수 있는 것입니다.
'싱글톤'은 '딱 하나뿐'이라는 뜻입니다. 그래서 이 장 제목이 "딱 하나뿐인 딸린 자원"입니다.
스마트폰으로 셋을 구분해 보자
| 스마트폰 비유 | 정체 | 특징 |
|---|---|---|
| 색상 "블랙" | 단순 속성 | 폰의 일부라 따로 관리 안 함 |
| 배터리 (충전량 85%) | 싱글톤 서브리소스 | 폰에 딱 하나, 폰과 함께 생기지만 상태는 따로 확인 |
| 설치된 앱들 (카톡, 유튜브…) | 일반 서브리소스(여러 개) | 각각 따로 설치·삭제, 개수 제한 없음 |
코드로 보면 이렇게 생겼습니다.
스마트폰 (부모 자원)
├── 색상: "블랙" ← 단순 속성
├── 배터리 ← 싱글톤 서브리소스 (하나뿐, 따로 조회 가능)
│ ├── 충전량: 85%
│ └── 건강도: 92%
└── 앱[] ← 일반 서브리소스 컬렉션 (여러 개)
속성 / 싱글톤 서브리소스 / 일반 자원 비교
| 항목 | 단순 속성 | 싱글톤 서브리소스 | 일반 자원 |
|---|---|---|---|
| 개수 | 해당 없음 | 부모당 1개 | 여러 개 가능 |
| 생성 | 부모와 함께 | 부모와 함께 자동 | 직접 만들어야 함 |
| 삭제 | 부모와 함께 | 부모와 함께 자동 | 직접 지워야 함 |
| 조회 | 부모 조회에 포함 | 따로 조회 가능 | 따로 조회 가능 |
| 수정 | 부모 수정에 포함 | 따로 수정 가능 | 따로 수정 가능 |
| 고유 주소(URL) | 없음 | 있음 (부모 주소 기반) | 있음 |
여기까지 체크포인트
- 싱글톤 서브리소스 = 속성과 자원의 하이브리드(중간 형태)다.
- 부모당 딱 하나만 존재한다.
- 생성·삭제는 속성처럼 자동, 조회·수정은 자원처럼 따로다.
12.3 어떻게 동작하나 — 표준 메서드
먼저, 이런 적 있죠?
새 자원을 만들면 으레 다섯 가지를 다 만들죠. 조회(Get), 생성(Create), 수정(Update), 삭제(Delete), 목록(List). 그런데 싱글톤 서브리소스에 다섯 개를 다 붙이면 이상한 게 생깁니다. "부모당 하나뿐인데 목록(List)이 왜 필요하지?" "부모를 만들면 자동으로 생기는데 생성(Create)을 따로 부르면?"
그게 바로 '일부만 동작한다'는 뜻이다
싱글톤 서브리소스는 다섯 메서드 중 조회와 수정만 평범하게 동작합니다. 나머지는 부모를 따라가거나 아예 없습니다.
| 표준 메서드 | 어떻게 동작? | 설명 |
|---|---|---|
| Get(조회) | 자원처럼 따로 | 고유 주소로 직접 조회 |
| Update(수정) | 자원처럼 따로 | 일부 칸만 골라 수정 가능 |
| Create(생성) | 속성처럼 자동 | 따로 못 만듦, 부모 생길 때 자동 생성 |
| Delete(삭제) | 속성처럼 자동 | 따로 못 지움, 부모 지울 때 자동 삭제 |
| List(목록) | 없음 | 부모당 1개뿐이라 목록이 의미 없음 |
Get과 Update — 자원처럼 따로
조회와 수정은 부모 주소가 아니라 자기 고유 주소를 씁니다.
# 조회 — 위치만 직접 가져옴
GET /drivers/1/location
→ { lat: 37.5665, long: 126.9780 }
# 수정 — 위도 한 칸만 바꿈
PATCH /drivers/1/location
Body: { lat: 37.5700 }
→ { lat: 37.5700, long: 126.9780 }
새 용어: PATCH — 자원의 일부 칸만 골라 고치는 HTTP 방식입니다. 일상 비유로 보면, 서류 전체를 새로 쓰는 게 아니라 틀린 한 줄만 수정 테이프로 고치는 것입니다. (전체를 통째로 바꾸는 PUT과 대비됩니다.)
핵심은 주소가 drivers/1(부모)이 아니라 drivers/1/location(자기 자신)이라는 점입니다.
잠깐 — 어느 칸을 고칠지 서버가 어떻게 알까? (fieldMask)
방금 PATCH로 위도 한 칸만 바꿨죠. 그런데 생각해 보면 이상합니다. 서버 입장에서는 "위도만 바꾸고 경도는 그대로 두라"는 건지, "경도는 비우라(null로 만들라)"는 건지 헷갈릴 수 있습니다.
# 헷갈리는 상황 — 위도만 보냈는데...
PATCH /drivers/1/location
Body: { lat: 37.5700 }
→ 서버: "경도(long)는 안 보냈네? 비우라는 건가, 그대로 두라는 건가?"
이 헷갈림을 없애는 표준 방식이 바로 fieldMask입니다.
새 용어: 필드 마스크(fieldMask) — PATCH로 일부만 고칠 때 "이 칸들만 건드려라"고 서버에 콕 집어 알려주는 목록입니다. 8장(부분 업데이트)에서 다룬 그 표준 방식이며, 싱글톤 서브리소스 수정에도 똑같이 씁니다. 일상 비유로 보면, 서류를 맡기면서 "3번 줄하고 7번 줄만 고쳐 주세요"라고 포스트잇으로 콕 집어 붙여 주는 것입니다. 안 적은 줄은 손도 안 댑니다.
fieldMask에 적힌 칸만 고치고, 안 적힌 칸은 그대로 둡니다.
# fieldMask로 "lat만 고쳐라"고 콕 집음
PATCH /drivers/1/location
Body: {
driverLocation: { lat: 37.5700 },
fieldMask: ["lat"] ← lat 한 칸만 건드려라
}
→ { lat: 37.5700, long: 126.9780 } ← long은 손도 안 댐, 그대로
비유 → API → 위험을 표로 묶으면 이렇습니다.
| 일상 비유 | API/코드 동작 | 위험·주의 |
|---|---|---|
| 포스트잇으로 "3번 줄만 고쳐 주세요" | fieldMask: ["lat"] 로 lat만 지정 | 적은 칸만 바뀜, 안 적은 칸은 보존 |
| 포스트잇 없이 서류를 통째로 맡김 | fieldMask 없이 PATCH | 서버가 빠진 칸을 비울지 둘지 헷갈림 → 데이터 유실 위험 |
잘못된 예와 올바른 예를 나란히 보면 차이가 분명합니다.
# 잘못된 예 — fieldMask 없이 일부만 보냄
PATCH /drivers/1/location
Body: { lat: 37.5700 }
→ 위험: 서버가 long을 "비우라"로 해석하면 long이 null로 날아감
# 올바른 예 — fieldMask로 고칠 칸을 명시
PATCH /drivers/1/location
Body: { driverLocation: { lat: 37.5700 }, fieldMask: ["lat"] }
→ lat만 안전하게 바뀌고 long은 그대로 보존
요약하면, 부분 수정(PATCH)에는 "어느 칸을 고칠지" 알려주는 fieldMask를 함께 보내는 것이 표준입니다.
Create — 부모 만들면 자동으로 따라옴
잘못된 기대 vs 올바른 동작을 비교해 봅시다.
# 잘못된 기대 — 이런 메서드는 없습니다
POST /drivers/1/location ← CreateDriverLocation? 존재하지 않음
# 올바른 동작 — 부모를 만들면 자동 생성
POST /drivers { name: "홍길동", licensePlate: "12가3456" }
→ Driver { id: "drivers/1", ... }
# 만들자마자 바로 조회됨 (이미 존재함, 기본값 상태)
GET /drivers/1/location
→ { lat: null, long: null } ← 기본값으로 초기화돼 있음
Delete — 부모 지우면 같이 사라짐
새 용어: 캐스케이드 삭제(cascade delete) — 부모를 지우면 딸린 자식까지 줄줄이 함께 지워지는 동작입니다. 일상 비유로 보면, 건물을 철거하면 그 안의 방들도 같이 사라지는 것과 같습니다.
# 위치가 있다
GET /drivers/12345/location
→ { lat: ..., long: ... }
# 부모 기사를 지운다
DELETE /drivers/12345
→ 200 OK
# 위치도 같이 사라졌다
GET /drivers/12345/location
→ 404 Not Found
핵심: 싱글톤 서브리소스는 따로 지우는 메서드가 없습니다. 부모가 사라지면 캐스케이드 삭제로 함께 사라집니다.
비유로 한눈에 — 표로 묶기
| 일상 비유 | API/코드 동작 | 위험·주의 |
|---|---|---|
| 배터리 충전량만 따로 확인 | GET /drivers/1/location | 부모 주소가 아닌 자기 주소를 써야 함 |
| 폰을 사면 배터리도 같이 들어 있음 | Driver 생성 시 Location 자동 생성 | CreateDriverLocation을 따로 만들면 안 됨 |
| 폰을 버리면 배터리도 같이 버려짐 | Driver 삭제 시 Location 캐스케이드 삭제 | 따로 삭제 메서드를 두면 일관성이 깨짐 |
12.3.2 지우는 대신 '초기화' — reset
먼저, 이런 적 있죠?
"위치 정보를 비우고 싶다"는 요구가 들어옵니다. 습관적으로 DELETE를 떠올리죠. 그런데 싱글톤 서브리소스는 부모가 살아 있는 한 지울 수가 없습니다. 지우는 메서드 자체가 없으니까요.
그게 바로 'reset'이 필요한 이유다
새 용어: 사용자 메서드(custom method) — 표준 5종(조회·생성·수정·삭제·목록)으로 표현하기 어려운 특별한 동작에 이름을 붙여 만든 메서드입니다. 일상 비유로 보면, 일반 버튼들(전원·볼륨) 말고 따로 만든 '초기화' 버튼 같은 것입니다.
새 용어: reset 메서드 — 자원을 공장 출고 상태(기본값)로 되돌리는 사용자 메서드입니다. 일상 비유로 보면, TV를 버리는 게 아니라 설정만 '공장 초기화'하는 버튼입니다.
지우는 게 아니라 기본값으로 되돌리는 것이라서 이름도 delete가 아니라 reset입니다.
# 지금 위치가 있다
GET /drivers/1/location
→ { lat: 37.5665, long: 126.9780 }
# 초기화한다 (지우는 게 아니라 비움)
POST /drivers/1/location:reset
→ { lat: null, long: null }
비유로 보면, TV 자체를 버리는 게 delete, 설정만 공장 초기화하는 게 reset입니다. 싱글톤 서브리소스에는 delete가 없으니 초기화는 reset으로 합니다.
비유로 한눈에 — 표로 묶기
| 일상 비유 | API/코드 동작 | 위험·주의 |
|---|---|---|
| TV 자체를 버림 | DELETE | 싱글톤 서브리소스엔 이 동작이 없음 |
| TV 설정만 공장 초기화 | POST /drivers/1/location:reset | 비우려면 삭제가 아니라 reset을 써야 함 |
12.3.3 어디에 두나 — 계층 구조 규칙
표준 규칙 (기본 안전판)
싱글톤 서브리소스를 둘 위치는 규칙이 단순합니다.
규칙 1 — 반드시 부모 밑에 둔다.
✅ /drivers/1/location ← 부모(Driver) 밑에 있음 (정답)
❌ /location ← 최상위에 혼자 있음 (금지)
새 용어: 전역 공유 잠금(global shared lock) — 모든 사용자가 같은 하나의 자원을 함께 쓰려고 줄 서는 병목 상황입니다. 일상 비유로 보면, 온 동네가 화장실 한 칸을 같이 쓰는 것과 같습니다.
최상위에 혼자 두면 모든 클라이언트가 그 하나를 공유하게 됩니다. 그러면 모든 쓰기가 한 곳으로 몰려 전역 공유 잠금이라는 병목이 생깁니다.
규칙 2 — 싱글톤 서브리소스는 또 다른 자식의 부모가 되면 안 된다.
✅ /drivers/1/location ← Driver → Location (정답)
❌ /drivers/1/location/signal ← Location이 또 부모가 됨 (금지)
싱글톤이 누군가의 부모 노릇을 하기 시작하면, 그 순간 완전한 부모 자원처럼 무거운 책임을 다 떠안아야 합니다. 중간 형태라는 단순함이 깨지는 것이죠.
비유로 한눈에 — 표로 묶기
| 일상 비유 | API/코드 상황 | 위험·주의 |
|---|---|---|
| 배터리는 폰 안에만 있음 | /drivers/1/location | 최상위에 혼자 두면 안 됨 |
| 온 동네가 화장실 한 칸 공유 | /location (최상위) | 전역 공유 잠금 병목 발생 |
| 배터리 밑에 또 부품을 매다는 격 | /drivers/1/location/signal | 싱글톤이 부모 노릇을 하면 단순함이 깨짐 |
전체를 합친 최종 모습
마지막으로 지금까지 배운 걸 한 API 정의로 묶어보겠습니다.
Ride Sharing API (v1)
// Driver — 다섯 표준 메서드 다 있음
GET /drivers (목록)
POST /drivers (생성)
GET /drivers/1 (조회)
PATCH /drivers/1 (수정)
DELETE /drivers/1 (삭제)
// DriverLocation — 조회·수정만 있음
GET /drivers/1/location (조회)
PATCH /drivers/1/location (수정)
// Create/Delete/List 없음 → 부모를 따라가거나 의미 없음
자원 정의는 이렇게 갈라집니다.
Driver { ← 부모: 위치가 여기 없음
id, name, licensePlate
}
DriverLocation { ← 싱글톤 서브리소스: 부모 주소에 딸림
id: "drivers/1/location"
lat, long, updateTime
}
잠깐 — 매초 바뀌는 위치에 updateTime이 왜 붙지?
위 정의를 보면 위치 칸 옆에 updateTime이 슬쩍 끼어 있습니다.
"위치는 1초마다 바뀌는데, 굳이 '마지막으로 바뀐 시각'까지 적어둘 필요가 있나?" 싶죠.
새 용어: updateTime — 이 자원이 마지막으로 갱신된 시각을 담는 칸입니다. 일상 비유로 보면, 버스 도착 안내 전광판에 뜨는 "방금 전 갱신"이라는 시각 표시와 같습니다. 숫자(도착 시간)만 보면 그게 1초 전 정보인지 5분 전 멈춘 정보인지 알 수 없으니, 갱신 시각을 같이 보여 주는 것입니다.
휘발성이 높은(자주 바뀌는) 데이터일수록 오히려 updateTime이 더 중요합니다. 받은 좌표가 방금 것인지, 신호가 끊겨 10초 전에 멈춘 것인지 구분해야 하기 때문입니다.
# 받은 응답
GET /drivers/1/location
→ { lat: 37.5665, long: 126.9780, updateTime: "2026-06-04T10:00:03Z" }
↑ 좌표만 보면 최신인지 알 수 없지만,
updateTime을 보면 "3초 전 갱신"이라 살아있는 위치임을 안다
요약하면, 자주 바뀌는 데이터일수록 그 값이 '언제 것인지'가 중요하므로 updateTime을 함께 둡니다.
여기까지 체크포인트
- Get/Update만 자원처럼 따로 동작한다.
- Create/Delete는 부모를 따라 자동 처리된다.
- 삭제 대신 reset으로 초기화한다.
- 반드시 부모 밑에 두고, 최상위는 금지한다.
- 싱글톤 서브리소스는 또 다른 자식의 부모가 될 수 없다.
12.4 떼어낼 때 생기는 두 가지 대가 (트레이드오프)
분리는 좋은 점만 있는 게 아닙니다. 대가가 둘 있습니다.
12.4.1 동시에 못 바꾼다 (원자성 없음)
새 용어: 원자성(atomicity) — 여러 작업이 한 묶음으로 '전부 성공 아니면 전부 실패'가 되는 성질입니다. 일상 비유로 보면, 계좌 이체에서 '내 통장 출금'과 '상대 통장 입금'이 둘 다 되거나 둘 다 안 되는 것과 같습니다.
부모와 싱글톤 서브리소스는 한 번의 요청으로 동시에 못 바꿉니다.
# 이런 한 방 요청은 불가능
"이름 바꾸면서 동시에 위치도 바꿔줘"
# 요청을 둘로 나눠야 함
PATCH /drivers/1 { name: "새이름" }
PATCH /drivers/1/location { lat: 37.5665 }
그런데 이건 사실 의도된 결과입니다. 애초에 떼어낸 이유가 "따로 관리하려고"였으니까요. 만약 둘을 꼭 동시에 바꿔야 한다면, 그건 애초에 떼어내면 안 되는 데이터였다는 신호입니다.
12.4.2 같은 종류는 부모당 하나만
새 용어: 서브컬렉션(sub-collection) — 부모에 딸린 자식들을 여러 개 담는 묶음입니다. 일상 비유로 보면, 한 사람(부모)에게 신용카드가 여러 장(자식 묶음) 있는 것과 같습니다.
싱글톤은 '딱 하나'가 핵심입니다. 같은 종류를 부모당 두 개 이상 두고 싶다면, 그건 싱글톤이 아니라 일반 서브컬렉션입니다.
✅ /drivers/1/location ← 위치 1개 (싱글톤, 정답)
❌ /drivers/1/locations ← 위치 여러 개 (이건 일반 서브컬렉션)
여러 개가 필요하면 싱글톤을 억지로 쓰지 말고 일반 서브컬렉션으로 설계해야 합니다.
비유로 한눈에 — 표로 묶기
| 일상 비유 | API/코드 상황 | 위험·주의 |
|---|---|---|
| 출금·입금이 한 묶음으로만 됨 | 부모+서브리소스 동시 수정 | 한 요청으로 못 함 → 요청 둘로 나눠야 함 |
| 카드가 여러 장 필요함 | /drivers/1/locations | 여러 개면 싱글톤이 아니라 서브컬렉션 |
12.5 직접 풀어보기 — 빈칸에서 독립까지
(1) 함께 풀기 (worked example)
문제: 운전기사의 위치를 조회하는 요청은?
풀이: 위치는 싱글톤 서브리소스이고, 조회는 자기 고유 주소로 합니다.
→ GET /drivers/1/location
(2) 빈칸 채우기 (부분 완성)
문제: 운전기사의 위치를 기본값으로 되돌리려면? 싱글톤 서브리소스에는 delete가 없으니, ___ 사용자 메서드를 씁니다.
→ POST /drivers/1/location:____
(채울 말: reset)
(3) 혼자 풀기 (독립 적용)
문제: 직원 자원에서 급여만 인사팀에게 보이게 하고 싶습니다. 어떻게 설계할까요?
정답을 보기 전에 직접 생각해 보세요. 힌트: 12.1에서 배운 '떼어내는 이유 3가지' 중 무엇에 해당하나요?
A: 보는 사람(권한)이 다른 데이터이므로 '보안' 이유로 분리합니다. 급여를 싱글톤 서브리소스로 떼어내, GetEmployee는 누구나, GetEmployeeSalary는 인사팀만 조회하도록 권한을 다르게 둡니다.
책 속 연습문제 Q&A
Q1. 속성이 어느 정도 크면 떼어내는 게 좋을까?
딱 떨어지는 숫자 기준은 없습니다. 부모 자원 크기의 대부분을 차지하거나, 대부분의 호출에서 필요 없는 데이터라면 분리를 고려합니다. 예를 들어 ACL이 규칙 수천 개를 가지면 분리 대상입니다. (새 용어: ACL(접근 제어 목록) — 누가 어떤 자원에 접근할 수 있는지 적어둔 명단입니다. 일상 비유로 보면, 행사장 입구의 '입장 가능자 명단'과 같습니다.)
Q2. 왜 조회와 수정만 지원하나?
부모가 있는 한 항상 존재해야 해서 따로 만들거나 지울 수 없기 때문입니다. 생성은 부모 만들 때 자동, 삭제는 부모 지울 때 자동입니다.
Q3. 왜 삭제 대신 reset인가?
싱글톤 서브리소스는 부모가 살아 있는 한 삭제 자체가 불가능합니다. 그래서 '초기화'가 필요하면 기본값으로 되돌리는 reset이 의미상 정확합니다.
Q4. 왜 싱글톤은 다른 싱글톤의 부모가 못 되나?
싱글톤이 부모 노릇을 하면 완전한 부모 자원처럼 무거운 책임을 다 떠안아야 합니다. 이는 '중간 형태'라는 단순함을 깨뜨립니다.
한 걸음 더 ▸ (지금 몰라도 됨)
- 떼어낼 크기 기준은 정답이 없습니다. 실무에서는 "대부분의 호출에서 쓰이지 않는가"를 더 중요한 신호로 봅니다.
- 표준 규칙은 "부모 밑에, 조회·수정만, 삭제는 reset"입니다. 예외 상황의 미세 조정은 실무에서 사례별로 다릅니다.
- 더 깊이 보고 싶다면 Google AIP-156(싱글톤 자원 설계 지침)을 참고하세요: https://google.aip.dev/156 (검증일 2026-06-04, searxng — 문서 존재 및 주제 일치 확인)
마무리 한 장 요약
| 질문 | 답 |
|---|---|
| 언제 떼어내나? | 크기·보안·휘발성이 다를 때 |
| 떼어낸 건 뭔가? | 속성과 자원의 중간, 싱글톤 서브리소스 |
| 어떻게 동작하나? | 조회·수정만 따로, 생성·삭제는 부모 따라 자동, 목록 없음 |
| 비우려면? | 삭제가 아니라 reset |
| 어디에 두나? | 반드시 부모 밑, 최상위 금지, 자기는 부모 못 됨 |
| 대가는? | 부모와 동시 수정 불가, 같은 종류 하나만 |
다음 장 예고: 다음 장에서는 자식이 부모를 바꿔 옮겨 다는 방법을 다룹니다. 지금은 몰라도 됩니다.
클릭하거나 Space를 눌러 뒤집기